# coding:utf-8
# 王一舟
# keywords.py
import json

import allure
import jsonpath
import requests
import pymysql
from pymysql import cursors
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import base64
from core.globalContext import g_Context  # 导入全局变量类
from deepdiff import DeepDiff  # 全量断言导包
from deepdiff import DeepSearch


# 存放关键字的文件:把所有常用的接口请求过程中的操作都放在这个位置
# 面向对象的思路


# 考虑:1.发送请求 post , get
# 添加@allure.step("描述信息")


class KeyWords():
    # 用来发送请求
    def __init__(self):  # 构造方法初始化一个会话
        self.request = requests.session()

    @allure.step("参数数据:发送post请求-json传参")
    def request_post_form_urlencoded(self, **kwargs):
        # 将传进来的读取yaml的参数逐个获取并改名字符合requests发送的要求
        url = kwargs.get("URL", None)
        params = kwargs.get("PARAMS", None)
        headers = kwargs.get("HEADERS", None)
        data = kwargs.get("DATA", None)
        files = kwargs.get("FILES", None)  # 上传文件 如:{"字段名":open("./files/20240715XXXX.png","rb")}
        # 把这些参数重新整合改名后拆包传给request发送请求

        #因为eval()不能转换None,所以下面做判断,如果files没有传进来,取值是None,那么不传files
        if files == None:
            request_data = {
                "url": url,
                "params": params,
                "headers": headers,
                "json": data,
            }
        else:
            request_data = {
                "url": url,
                "params": params,
                "headers": headers,
                "json": data,
                "files": eval(files)  # 变成字典格式
            }

        # 发送post
        response = self.request.post(**request_data)
        # 将响应的对象存储到全局变量中,以供下面的ex_jsonData方法提取数据
        g_Context().set_dict("current_response", response)

        print("-" * 20)
        print(response.json())  # 打印全局字典中的内容
        print("-" * 20)
        # 将响应结果信息返回
        return response

    @allure.step("参数数据:发送post请求-data传参")
    def request_post_form_data(self, **kwargs):
        # 将传进来的读取yaml的参数逐个获取并改名字符合requests发送的要求
        url = kwargs.get("URL", None)
        params = kwargs.get("PARAMS", None)
        headers = kwargs.get("HEADERS", None)
        data = kwargs.get("DATA", None)
        files = kwargs.get("FILES", None)  # 上传文件用 如:{"字段名":open("./files/20240715XXXX.png","rb")}
        # 把这些参数重新整合改名后拆包传给request发送请求

        # 因为eval()不能转换None,所以下面做判断,如果files没有传进来,取值是None,那么不传files
        if files ==None:
            request_data = {
                "url": url,
                "params": params,
                "headers": headers,
                "data": data,
            }
        else:
            request_data = {
                "url": url,
                "params": params,
                "headers": headers,
                "data": data,
                "files": eval(files)  # 变成字典格式
            }

        # 发送post
        response = self.request.post(**request_data)

        # 将响应的对象存储到全局变量中,以供下面的ex_jsonData方法提取数据
        g_Context().set_dict("current_response", response)
        print("-" * 20)
        print(response.json())  # 打印全局字典中的内容
        print("-" * 20)
        # 将响应结果信息返回
        return response

    @allure.step("参数数据:发送get请求")
    def request_get(self, **kwargs):
        url = kwargs.get("URL", None)
        params = kwargs.get("PARAMS", None)
        headers = kwargs.get("HEADERS", None)

        # 把这些参数重新整合改名后拆包传给request发送请求
        request_data = {
            "url": url,
            "params": params,
            "headers": headers,
            # "data": data, 一般情况下get请求是不传data参数的
        }
        # 发送post
        response = self.request.get(**request_data)
        # 将响应的对象存储到全局变量中,以供下面的ex_jsonData方法提取数据
        g_Context().set_dict("current_response", response)
        print("-" * 20)
        print(response.json())  # 打印全局字典中的内容
        print("-" * 20)
        # 将响应结果信息返回
        return response

    @allure.step("参数数据:发送delete请求")
    def request_delete(self, **kwargs):
        # 将传进来的读取yaml的参数逐个获取并改名字符合requests发送的要求
        url = kwargs.get("URL", None)
        params = kwargs.get("PARAMS", None)
        headers = kwargs.get("HEADERS", None)
        data = kwargs.get("DATA", None)
        request_data = {
            "url": url,
            "params": params,
            "headers": headers,
            "data": data
        }
        # 把这些参数重新整合改名后拆包传给request发送请求
        response = self.request.delete(**request_data)
        # 将响应的对象存储到全局变量中,以供下面的ex_jsonData方法提取数据
        g_Context().set_dict("current_response", response)
        print("-" * 20)
        print(response.json())  # 打印全局字典中的内容
        print("-" * 20)
        # 将响应结果信息返回
        return response

    '''
    要实现多文件的接口关联,就需要把关联参数放在一个统一位置(全局变量)
    '''

    @allure.step("参数数据:提取json数据并存储到全局变量中")
    def ex_jsonData(self, **kwargs):
        '''
        使用jsonpath的方式提取响应数据中的接口关联数据
        1.提取的数据是什么
        2.提取的表达式是什么
        3.提取的下标是什么
        4.保存到全局变量中的变量名是什么
        :param EXVALUE: jsonpath提取表达式
        :param INDEX: jsonpath的下标 默认为0
        :param VARNAME: 将数据保存到全局变量时,定义的变量名(key)
        :return:
        '''
        # TODO 1:拿到响应数据
        response_json_data = g_Context().get_dict('current_response').json()
        # TODO 2: 拿到jsonpath提取表达式
        EXVALUE = kwargs.get("EXVALUE", "需要传入jsonpath表达式")
        # TODO 3: 拿到jsonpath提取式的下标
        INDEX = kwargs.get("INDEX", 0)
        # dict.get()的第二个参数为可选参数，当key不存在时返回的默认值。如果不提供，则返回None。
        # 所以 kwargs.get("INDEX",0) 就代表当INDEX不传值的时候 , 值默认为0
        if INDEX is None:  # 如果INDEX传进来是None 也把它变成0
            INDEX = 0
        # TODO 4: 拿到保存到全局变量中的变量名
        VARNAME = kwargs.get('VARNAME', "需要传入保存至全局变量时的key名称")  # 将数据保存到全局变量时,定义的变量名(key)

        # TODO 5: 提取数据
        ex_data = jsonpath.jsonpath(response_json_data, EXVALUE)[INDEX]
        # TODO 6: 把提取的数据保存到全局变量并指定变量名
        g_Context().set_dict(VARNAME, ex_data)

        print("-" * 20)
        print("全局变量中的内容:", g_Context().show_dict())  # 打印全局字典中的内容
        print("-" * 20)
        return ex_data  # 返回提取的数据

    # 查询数据库, 并将结果存到全局变量 -- 用于后续引用/断言
    @allure.step("提取MySQL数据库数据并存储到全局变量")
    def ex_mysqlData(self, **kwargs):
        '''
        查询数据库,并将结果存到全局变量
        :param 数据库: 要使用的数据库名
        :param SQL: SQL语句
        :param 引用变量: 将数据保存到全局变量时,定义的变量名(key),传进来时是个字符串 如"id,username,nickname",注意!:传进来的这些变量名,一定要和sql查询结果的key一致!
        :return:
        '''
        # 数据库的连接信息conn_data{}
        conn_data = {"cursorclass": cursors.DictCursor}  # 连接参数的字典中先写入cursors.DictCursor,表示:sql执行结果以[{},{}]的格式显示
        # 由于数据的连接配置信息都写在了config.yaml中,项目执行时会第一时间写到全局变量,所以从全局变量中提就行了
        db_config = g_Context().get_dict("_database").get(
            kwargs["数据库"])  # 数据库的用例中需要写明用哪个数据库,通过这个参数从全局变量中提取对应的连接参数,这里直接字典提取
        '''
        如: 数据库名是mysql001的话 就从全局变量中提取下面的配置信息
        db_config = {'host': 'shop-xo.hctestedu.com', 'port': 3306, 'user': 'api_test', 'password': 'Aa9999!', 'db': 'shopxo_hctested', }
        '''
        conn_data.update(db_config)  # 再把数据库配置文件的字典update到conn_data,组成完整的数据库连接参数
        # 创建数据库连接对象
        connection = pymysql.connect(**conn_data)  # 拆包传参
        # 创建游标对象
        cur = connection.cursor()
        # 执行SQL语句
        cur.execute(kwargs["SQL"])
        # 获取执行结果
        sql_results = cur.fetchall()  # 获取的是一个[{},{},{},{}...]的格式
        print("数据库读取的数据是:",sql_results) #如[{'id': 17889, 'username': '15920532568', 'nickname': ''}, {'id': 31646, 'username': '15921953117', 'nickname': '加油啦!!!'}]
        cur.close()  # 关闭连接
        # connection.close()#关闭连接
        '''
        思考:
            1.数据库返回的数据有多个 -- 设想 和jmeter一样 [变量加下标]的方式进行存储到全局变量
            2.公司不一定只有一个数据库,可能多个,所以可以在config.yaml中写入多个数据库的连接信息,然后执行的时候写到全局变量中的
        '''
        var_names = kwargs["引用变量"].split(",")  # 比如传入的变量名是 id,username ; 通过逗号分隔,获取一个列表'
        # 注意kwargs["引用变量"]传进来的变量名必须和sql查询结果中的key一致,否则下面算法会取不到值!

        result = {}  # 例{'id_1': '11', 'user_1': 'hami', 'age_1': 13, 'id_2': '12', 'user_2': 'lll', 'age_2': 14, 'id_3': '13', 'user_3': 'kkk', 'age_3': 15}
        for i, res in enumerate(sql_results, start=1):  # 遍历数据库查询的数据[{},{},{},{}...] 每次遍历一个字典外加一个计数
            for j, value in enumerate(var_names):  # 遍历传进来的变量名如:[id,username,nickname]
                result[f"{var_names[j]}_{i}"] = res.get(
                    var_names[j])  # var_names[下标]_计数 = sql_results当前遍历的字典.get(var_names[下标]),
                # 因此 var_names 中的变量名必须和数据库查出来的数据中的key一致,否则无法从数据库查询数据中找到对应的变量名,从而无法赋值

        g_Context().update_dict(result)  # 写到全局变量
        print("-" * 20)
        print("全局变量中的内容:", g_Context().show_dict())  # 打印全局字典中的内容
        print("-" * 20)

    # 断言-骚操作
    @allure.step("断言处理: 通过响应数据进行断言")
    def assert_text_comparators(self, **kwargs):
        '''
        :param EXPECTED: 期望结果
        :param VALUE: 实际结果 写全局变量中对应值的变量名 如"{{msg_success}}"
        :param OP_STR: 比较运算符 > < == in
        :return:
        '''
        # 字典+匿名函数 :lambda 需要传的参数 : 执行的操作
        comparators = {
            # KEY: VALUE
            # 运算符是A: 就执行A
            ">": lambda a, b: a > b,
            "<": lambda a, b: a < b,
            "==": lambda a, b: a == b,
            ">=": lambda a, b: a >= b,
            "<=": lambda a, b: a <= b,
            "!=": lambda a, b: a != b,
            "in": lambda a, b: a in b,
        }
        if kwargs["OP_STR"] not in comparators:  # 如果传进来的运算符不在comparators中
            raise ValueError(f"断言运算符: {kwargs['OP_STR']}不在既定算法内,或运算符写错了")

        if not comparators[kwargs["OP_STR"]](kwargs["VALUE"], kwargs["EXPECTED"]):  # 从字典中直接获取对应的lambda函数, 然后在后面+ ()表示执行
            '''
            kwargs["OP_STR"]就是传进来的算法 , comparators[kwargs["OP_STR"]] 就是获取字典中对应的比较算法 ,后面加上()代表执行这个lambda函数
            comparators[kwargs["OP_STR"]](lambda参数a,lambda参数b)
            '''
            # 返回的是一个flase,才会进入这里
            raise AssertionError(f"{kwargs['VALUE']} {kwargs['OP_STR']} {kwargs['EXPECTED']},失败")


    # TODO: 扩展 - JSOND断言方法
    @allure.step("参数数据：JSOND断言文本内容")
    def assert_json_comparators(self, **kwargs):
        """
        直接通过在全局变量中的响应对象current_response.json, 获取所有响应结果
        通过传入的jsonpath表达式获取要断言的实际结果,和预期结果做对比
        好处:用例中可以省略 ex_jsonData,不再需要提取json数据保存到全局变量
        只要是响应数据是json数据,可以用这个做断言


        参数:
        VALUE (Any): 要比较的jsonPath值。 直接写jsonpath表达式
        EXPECTED (Any): 预期的值。
        OP_STR (str): 操作符的字符串表示（如 '>', '<', '==' 等）。
        MESSAGE (str, optional): 自定义的错误消息。
        返回:
        None: 如果断言成功，则不返回任何内容。
        引发:
        AssertionError: 如果断言失败。
        """
        comparators = {
            '>': lambda a, b: a > b,
            '<': lambda a, b: a < b,
            '==': lambda a, b: a == b,
            '>=': lambda a, b: a >= b,
            '<=': lambda a, b: a <= b,
            '!=': lambda a, b: a != b,
        }

        message = kwargs.get("MESSAGE", None) #自定义的错误信息,写了就返回这个,不写返回else的

        if kwargs["OP_STR"] not in comparators:
            raise ValueError(f"没有该操作方式: {kwargs['OP_STR']}")

        # 通过jsonpath获取对应的数据
        # 获取响应数据
        response = g_Context().get_dict("current_response").json()
        ex_data = jsonpath.jsonpath(response, kwargs['VALUE'])[0] # 默认就取第一个

        if not comparators[kwargs['OP_STR']](ex_data, kwargs["EXPECTED"]):
            if message:
                raise AssertionError(message)
            else:
                raise AssertionError(f"{ex_data} {kwargs['OP_STR']} {kwargs['EXPECTED']} 失败")





    # TODO 扩展, 对数据进行AES对称加密
    @allure.step("加密处理:把数据进行AES对称加密")
    def encrypt_aes(self, **kwargs):
        # 扩展: 签名:身份的认证,
        # 一般的签名都是双重加密,在加密的基础上再去加密,其处理方式站在接口的角度和加密一样

        '''
        对数据进行AES加密,写到全局变量中
        :param data:需要加密的原始数据
        :param VARNAME: 将数据保存到全局变量时,定义的变量名(key)
        :return:
        '''
        # 创建AES对象，使用ECB模式和PKCS7填充

        if isinstance(kwargs['data'], int):
            kwargs['data'] = str(kwargs['data'])

        key = b'1234567812345678'
        cipher = AES.new(key, AES.MODE_ECB)
        # 数据需要是AES块大小的倍数（16字节），因此进行PKCS7填充
        padded_data = pad(kwargs['data'].encode('utf-8'), AES.block_size)
        # 加密数据
        encrypted_data = cipher.encrypt(padded_data)
        # 将加密数据编码为base64
        encrypt_data = base64.b64encode(encrypted_data).decode('utf-8')
        # 将加密后的东西放进全局变量
        g_Context().set_dict(kwargs['VARNAME'], encrypt_data)

    # TODO 扩展, DeepDiff全量断言
    @allure.step("DeepDiff全量断言")
    def assert_DeepDiff_json(self, **kwargs):
        '''
        对比2个数据的差异
        :param json1:旧参数
        :param json2:新参数
        :param 忽略排序: ignore_order=True , 默认Flase
        :param 忽略大小写:ignore_string_case=True 默认Flase
        :param 忽略字段:exclude_paths={} 默认None
        :return:
        '''
        json1 = kwargs["json1"]
        json2 = kwargs["json2"]

        # 对比条件,可能没有传递,用 get()
        # 过滤条件可以组装起来,变成 key : value的形式
        filters = {
            "ignore_order": kwargs.get("忽略排序", False),
            "ignore_string_case": kwargs.get("忽略大小写", False),
            "exclude_paths": kwargs.get("忽略字段", None)
        }
        diff_data = DeepDiff(json1, json2, **filters)
        # 断言 #因为期望值是空{},所以是反着来判断
        assert not diff_data, f"DeepDiff断言失败:{diff_data}"
        # return diff_data

    # TODO 扩展, DeepDiff搜索断言
    @allure.step("DeepDiff_search搜索断言")
    def assert_DeepDiff_search(self, **kwargs):
        '''
        对比2个数据的差异
        :param obj:搜素的范围数据
        :param item:要搜索的内容
        :param 大小写敏感: case_sensitive=True, 默认Flase 不敏感
        '''
        obj = kwargs["obj"]
        item = kwargs["item"]
        case_sensitive = kwargs.get("大小写敏感", False)
        filter = {"case_sensitive": case_sensitive}
        search_data = DeepSearch(obj=obj, item=item, **filter)

        if search_data:
            print(search_data)
        else:
            raise AssertionError(f"未找到{item}")
